/**
 * Copyright (C) 2010 Cloudfarming <info@cloudfarming.nl>
 *
 * Licensed under the Eclipse Public License - v 1.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.eclipse.org/legal/epl-v10.html
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package nl.cloudfarming.client.sensor.greenseeker;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import nl.cloudfarming.client.logging.AppLogFactory;
import nl.cloudfarming.client.logging.AppLogger;
import nl.cloudfarming.client.model.SensorDataFile;
import nl.cloudfarming.client.model.SensorMetadata;
import nl.cloudfarming.client.sensor.SensorProcessor;
import nl.cloudfarming.client.sensor.MalformedFileFormatException;
import nl.cloudfarming.client.sensor.SensorEventKey;
import nl.cloudfarming.eventbus.BaseGuiEventProducer;
import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.api.progress.ProgressHandleFactory;
import org.openide.util.NbBundle;

/**
 * Greenseeker File processor abstract class. Each inheriting Greenseeker file processor implements its own parse method and exposes header-length and recordlength through getter-methods.
 * @author Gerben Feenstra
 */
public abstract class GreenseekerFileProcessor extends SensorProcessor<GreenseekerSensorData> {

    // Messages
    private static final String LOGKEY_FAILED_TO_READ_FILE = "message.error.failed_to_read_file";
    private static final String LOGKEY_PROCESSING_SUMMARY_PROCESSEDMSG = "message.info.process_summary.processed";
    private static final String LOGKEY_PROCESSING_PARTIALLY_PROCESSEDMSG = "message.info.process_summary.partiallyprocessed";
    private static final String LOGKEY_PROCESSING_SUMMARY_ERRORMSG = "message.info.process_summary.errors";
    private static final String LOGKEY_PROCESSING_START = "message.info.process_start";
     
    // Constant int values
    private static final int BULK_COMMIT_AMOUNT = 100; // How many record should be gathered before doing a bulk-commit?
    private static final int PROGRESS_REPORT_INTERVAL = 5000; // How many record should be processed before printing a progress message in the log

    // Date format variables
    private static final String PROPERTY_MONTH = "MM";
    private static final String PROPERTY_YEAR = "YYYY";
    private static final String PROPERTY_YEAR_INTERNATIONAL = "YYYY";
    private static final String PROPERTY_DAY = "DD";


    // Other variable declarations
    private static final AppLogger LOG = AppLogFactory.getLogger(GreenseekerFileProcessor.class);
    private GreenseekerServiceImpl greenseekerService = new GreenseekerServiceImpl();
    private final ProgressHandle progressHandle;
    private final EventProducer eventProducer = new EventProducer();
    protected File fileToProcess;

    /**
     * Create a GreenseekerFileProcessor instance to process the specified greenseeker sensor file.
     *
     * @param toProcess The Greenseeker sensorfile to be processed
     */
    public GreenseekerFileProcessor(File fileToProcess) {
        assert fileToProcess != null;
        this.fileToProcess = fileToProcess;
        this.progressHandle = ProgressHandleFactory.createHandle(NbBundle.getMessage(this.getClass(), "greenseeker.processor.processDescription", fileToProcess.getName()));
    }

    abstract protected GreenseekerSensorData parse(String payload, SensorDataFile dataFile) throws GreenseekerParseException;
    
    abstract protected int getHeaderLength();
    
    abstract protected int getRecordLength();
    
    /**
     * Generic greenseeker method to process the specified file
     */
    @Override
    public void process() {   
        SensorDataFile dataFile = null;
        try {
            int success = 0;
            int fail = 0;
            int progress = 0;
            
            LOG.info(LOGKEY_PROCESSING_START, getLogDisplayName(), fileToProcess.getName());

            // Create Datafile
            dataFile = createSensorDataFile(fileToProcess);
            greenseekerService.newSensorDataFile(dataFile);

            List<String> recordList = new ArrayList<String>();

            // Create record byte[] container
            final byte[] record = new byte[getRecordLength()];

            // Create new BufferedInputStream
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileToProcess));

            // Skip header
            bis.skip(getHeaderLength());

            // Fill list
            while (bis.read(record) > 0) {
                recordList.add(new String(record));
            }

            List<GreenseekerSensorData> persistBuffer = new ArrayList<GreenseekerSensorData>();

            // Start progressbar
            this.progressHandle.start(recordList.size());

            // Start processing lines in list
            for (String line : recordList) {
                try {
                    persistBuffer.add(parse(line, dataFile));

                    // Persist
                    if (success++ % BULK_COMMIT_AMOUNT == 0) {
                        greenseekerService.persist(persistBuffer);
                        persistBuffer.clear();
                    }
                } catch (GreenseekerParseException ex) {
                    LOG.trace(ex);
                    fail++;
                } catch (Exception ex) {
                    LOG.trace(ex);
                    fail = fail + BULK_COMMIT_AMOUNT;
                    persistBuffer.clear();
                }

                progressHandle.progress(++progress);

                // Log partial progress at intervals
                if (progress % PROGRESS_REPORT_INTERVAL == 0) {
                    LOG.info(LOGKEY_PROCESSING_PARTIALLY_PROCESSEDMSG, progress);
                }
            }

            LOG.info(LOGKEY_PROCESSING_SUMMARY_PROCESSEDMSG, success);

            if(fail > 0){
                LOG.warning(LOGKEY_PROCESSING_SUMMARY_ERRORMSG, fail);
            }

            //Persist the remaining records
            greenseekerService.persist(persistBuffer);
            eventProducer.triggerEvent(SensorEventKey.SENSOR_DATA_IMPORTED);

            // Finish progressbar
            progressHandle.finish();
        } catch (Exception ex) {
            LOG.error(LOGKEY_FAILED_TO_READ_FILE, fileToProcess.getName());
            if (dataFile != null) {
                greenseekerService.removeSensorDataFile(dataFile);
            }
            LOG.trace(ex);
        }

    }

    /**
     * Generic greenseeker method to extract a date from a Greenseeker file name.
     *
     * @param fileName The filename to extract the date from
     * @return The extracted date
     * @throws MalformedFileFormatException Filename does not match the prescribed Greenseeker filenameformat.
     * @throws ParseException Exception occurs when the date cannot be parsed.
     */
    @Override
    protected Date getDateFromFileName(File sensorFile) throws MalformedFileFormatException, ParseException {
        String fileName = sensorFile.getName();
        String greenseekerDateFormat = GreenseekerPreference.SENSOR_DATA_FILE_FORMAT.getValue();
        assert !greenseekerDateFormat.isEmpty();

        if (greenseekerDateFormat.contains(PROPERTY_DAY) && greenseekerDateFormat.contains(PROPERTY_MONTH) && greenseekerDateFormat.contains(PROPERTY_YEAR)) {
            int numberOfYearCharacters = PROPERTY_YEAR.length();
            if (greenseekerDateFormat.contains(PROPERTY_YEAR_INTERNATIONAL)) {
                numberOfYearCharacters = PROPERTY_YEAR_INTERNATIONAL.length();
            }
            String day = fileName.substring(greenseekerDateFormat.indexOf(PROPERTY_DAY), greenseekerDateFormat.indexOf(PROPERTY_DAY) + 2);
            String month = fileName.substring(greenseekerDateFormat.indexOf(PROPERTY_MONTH), greenseekerDateFormat.indexOf(PROPERTY_MONTH) + 2);
            String year = fileName.substring(greenseekerDateFormat.indexOf(PROPERTY_YEAR), greenseekerDateFormat.indexOf(PROPERTY_YEAR) + numberOfYearCharacters);
            StringBuilder dateBuilder = new StringBuilder(day);
            dateBuilder.append(month);
            dateBuilder.append(year);

            SimpleDateFormat date = new SimpleDateFormat("ddMMyy");
            return date.parse(dateBuilder.toString());
        } else {
            StringBuilder sb = new StringBuilder("The property which specifies file format is malformed while creating date from filename: ");
            sb.append(fileName);
            throw new MalformedFileFormatException(sb.toString());
        }
    }

    /**
     * Generic greenseeker method to build a Greenseeker specific metadata object
     *
     * @return Greenseeker metadata object
     */
    @Override
    protected SensorMetadata buildMetadataFromPreferences() {
        String fileFormat = GreenseekerPreference.SENSOR_DATA_FILE_FORMAT.getValue();
        int sensorHeigth = Integer.parseInt(GreenseekerPreference.GREENSEEKER_SENSOR_HEIGTH.getValue());
        int numberOfSensors = Integer.parseInt(GreenseekerPreference.NUMBER_OF_GREENSEEKER_SENSORS.getValue());
        String greenseekerSensorSpread = GreenseekerPreference.GREENSEEKER_SENSOR_SPREAD.getValue();
        String sensorType = GreenseekerPreference.SENSOR_TYPE.getValue();
        String spectralChannels = GreenseekerPreference.SPECTRAL_CHANNELS.getValue();
        return new SensorMetadata(fileFormat, sensorHeigth, numberOfSensors, greenseekerSensorSpread, sensorType, spectralChannels);
    }

    /**
     * Generic greenseeker method to remove multiple spaces from payload String and split it into a String array
     *
     * @param payload The String to convert to an array
     * @return the splitted payload String
     */
    protected String[] getFields(String payload){
        String payloadSpaced = payload.replaceAll("\\s{1,}", " ");
        String payloadTrimmed = payloadSpaced.trim();

        return payloadTrimmed.split(GreenseekerFileDescriptionBorne.getDelimiter());
    }

    private class EventProducer extends BaseGuiEventProducer {

        public EventProducer() {
            super(SensorEventKey.class);
        }
    }
}
